package ru.amse.ilyin.parser;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import ru.amse.ilyin.analyzer.Class;
import ru.amse.ilyin.analyzer.Interface;
import ru.amse.ilyin.analyzer.impl.ClassImpl;
import ru.amse.ilyin.analyzer.impl.InterfaceImpl;

/**
 * Class file parser.<br>
 * Usage: create instance, call {@link #parse()} method, then
 * get necessary information using proper methods. 
 * 
 * @see "http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html"
 *
 * @author Alexander Ilyin
 */
public class ClassFileParser {
	private static final int ACC_INTERFACE = 0x0200;
	
	private final File filename;
	
	private boolean parsed = false;
	
	private final ConstantPool constantPool = new ConstantPool();
	
	private String name;
	
	private boolean isClass;
    
    private Class superclass;
    
    private final Set<Interface> superinterfaces = new HashSet<Interface>();
	
	/**
	 * Creates a class file parser by the specified filename 
	 * of the class file.
	 * 
	 * @param filename name of a class file to parse 
	 */
	public ClassFileParser(File filename) {
		this.filename = filename;
	}
	
	/**
	 * Returns a name of the class or interface described 
	 * by the class file.
	 * 
	 * @return a name of the class or interface
	 */
	public String getName() {
		checkIfParsed();
		
		return name;
	}
	
	/**
	 * Returns true, if class file describes a class;
	 * false, if class file describes an interface.
	 * 
	 * @return true, if the class file describes a class;
	 * 		   false, if class file describes an interface
	 */
	public boolean isClass() {
	    checkIfParsed();
	    
	    return isClass;
	}
    
	/*public Set<Package> getImportedPackages() {
		throw new UnsupportedOperationException("not implemented");
	}*/

	/**
	 * Returns a superclass of the class described by the class file.
	 * If class file specifies an interface, 
	 * <code>IllegalStateException</code> is thrown.
	 * 
	 * @return direct superclass
	 * @throws IllegalStateException if class file describes an interface
	 */
	public Class getSuperclass() {
        checkIfParsed();
        
        if (!isClass) {
            throw new IllegalStateException("not a class");
        }
        
        return superclass;
	}
	
	/**
	 * Returns direct superinterfaces of the class or interface
	 * described by the class file.
	 * 
	 * @return direct superinterfaces
	 */
	public Set<Interface> getSuperinterfaces() {
		checkIfParsed();
		
		return Collections.unmodifiableSet(superinterfaces);
	}
	
	private void checkIfParsed() {
		if (!parsed) {
			throw new IllegalStateException("class file hasn't been parsed");
		}
	}
	
	/**
	 * Parses a class file specified in the constructor.
	 *  
	 * @throws IOException if an i/o error occurs
	 */
	public void parse() throws IOException {
		final DataInputStream input = new DataInputStream(
				new BufferedInputStream(new FileInputStream(filename)));
		
		input.readInt();			// reading magic
		input.readUnsignedShort();	// reading minor_version
		input.readUnsignedShort();	// reading major_version

		constantPool.parse(input);

		// reading access_flags
		final int flags = input.readUnsignedShort();
		isClass = ((flags & ACC_INTERFACE) == 0) ? true : false;

		final int thisClass = input.readUnsignedShort();
		name = constantPool.getClassName(thisClass);
		
        final int superclassIndex = input.readUnsignedShort();
        if (superclassIndex == 0) {
            superclass = null;
        } else {
            superclass = new ClassImpl(constantPool.getClassName(superclassIndex));
        }
        
        final int interfaceCount = input.readUnsignedShort();
        for (int i = 0; i < interfaceCount; i++) {
            final String interfaceName = constantPool.getClassName(
            		input.readUnsignedShort());
            superinterfaces.add(new InterfaceImpl(interfaceName));
        }
        
		try {
			input.close();
		} catch(IOException e) {
			//TODO replace with logger
			System.out.println(
					"failed to close a stream: " + filename.getPath());
		}
		
		parsed = true;
	}
}
